package sk.uniza.fri.cerviix.server.network;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import sk.uniza.fri.cerviix.server.Server;
import sk.uniza.fri.cerviix.server.game.Player;
import sk.uniza.fri.cerviix.server.network.messages.client.ConnectMessage;
import sk.uniza.fri.cerviix.server.network.messages.client.DisconnectMessage;
import sk.uniza.fri.cerviix.server.network.messages.client.WormDirectionMessage;

public class Client
{
	private Player player = null;
	
	private NetworkManager networkManager;
	private Socket socket;
	
	private Input input;
	private Output output;
	
	public Client(NetworkManager networkManager, Socket socket) throws IOException
	{
		this.networkManager = networkManager;
		this.socket = socket;

		input = new Input(new DataInputStream(socket.getInputStream()));
		output = new Output(new DataOutputStream(socket.getOutputStream()));
		
		input.register(MessageType.CONNECT, ConnectMessage.class);
		input.register(MessageType.DISCONNECT, DisconnectMessage.class);
		input.register(MessageType.WORM_DIRECTION, WormDirectionMessage.class);
		
		input.start();
		output.start();
	}
	
	@Override
	protected void finalize() throws Throwable
	{
		super.finalize();
		
		close();
	}
	
	public void close()
	{
		Server.log.info(String.format("Closing Client associated with player %s", player.getName()));
		
		networkManager.removeClient(this);
		
		input.interrupt();
		output.interrupt();
		
		try
		{
			socket.close();
		}
		catch (IOException e)
		{
			Server.log.warning("Failed socket close.");
		}
	}
	
	public void attachPlayer(Player player)
	{
		if (this.player != null)
		{
			throw new IllegalStateException("Player is already attached.");
		}
		
		this.player = player;
	}
	
	public Player getPlayer()
	{
		return player;
	}
	
	public void sendMessage(ServerMessage message)
	{
		output.addMessage(message);
	}
	
	private synchronized void disconnect()
	{
		Server.log.warning("Client disconnected.");
		
		networkManager.removeClient(this);
	}
	
	private class Input extends Thread
	{
		private DataInputStream stream;
		
		private Map<Byte, Class<? extends ClientMessage>> messageClasses = new HashMap<Byte, Class<? extends ClientMessage>>();
		
		public Input(DataInputStream stream)
		{
			this.stream = stream;
		}
		
		public void register(MessageType type, Class<? extends ClientMessage> messageClass)
		{
			messageClasses.put(type.value, messageClass);
		}
		
		public void run()
		{
			while (!isInterrupted())
			{
				try
				{
					byte type = stream.readByte();
					
					if (!messageClasses.containsKey(type))
					{
						throw new RuntimeException("ClientMessage not registered: " + type);
					}
					
					try
					{
						Class<? extends ClientMessage> messageClass = messageClasses.get(type);
						ClientMessage message = messageClass.getConstructor(Client.class).newInstance(Client.this);
						
						message.attachStream(stream);
						message.receive();
						
						networkManager.receive(message);
					}
					catch (IOException e)
					{
						throw e;
					}
					catch (Exception e)
					{
						throw new RuntimeException("Can't instantiate ClientMessage.");
					}
				}
				catch (IOException e)
				{
					if (!isInterrupted())
					{
						disconnect();
						return;
					}
				}
			}
			
			Server.log.info("Client NetworkInput thread stopped normally.");
		}
	}
	
	private class Output extends Thread
	{
		private DataOutputStream stream;
		private BlockingQueue<ServerMessage> queue = new LinkedBlockingQueue<ServerMessage>();
		
		public Output(DataOutputStream stream)
		{			
			this.stream = stream;
		}
		
		public void addMessage(ServerMessage message)
		{
			queue.add(message);
		}
		
		public void run()
		{
			try
			{
				while (true)
				{
					ServerMessage message = queue.take();
					
					message.attachStream(stream);
					
					try
					{
						message.send();
					}
					catch (IOException e)
					{
						disconnect();
						return;
					}
				}
			}
			catch (InterruptedException e)
			{
				Server.log.info("Client NetworkOutput thread stopped normally.");
			}
		}
	}
}
